/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.core.io;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

/**
 * Default implementation of the {@link ResourceLoader} interface.
 * Used by {@link ResourceEditor}, and serves as base class for
 * {@link org.springframework.context.support.AbstractApplicationContext}.
 * Can also be used standalone.
 *
 * <p>Will return a {@link UrlResource} if the location value is a URL,
 * and a {@link ClassPathResource} if it is a non-URL path or a
 * "classpath:" pseudo-URL.
 * <p/>
 * eg:
 * <p>
 * ResourceLoader resourceLoader = new DefaultResourceLoader();
 * <br/>
 * Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt");
 * System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource));
 * <br/>
 * Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt");
 * System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource));
 * <br/>
 * Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt");
 * System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource));
 * <br/>
 * Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");
 * System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof  UrlResource));
 * <p/>
 * @author Juergen Hoeller
 * @since 10.03.2004
 * @see FileSystemResourceLoader
 * @see org.springframework.context.support.ClassPathXmlApplicationContext
 */
public class DefaultResourceLoader implements ResourceLoader {

	@Nullable
	private ClassLoader classLoader;

	private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

	private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);


	/**
	 * Create a new DefaultResourceLoader.
	 * <p>ClassLoader access will happen using the thread context class loader
	 * at the time of this ResourceLoader's initialization.
	 *
	 * 在使用不带参数的构造函数时，使用的 ClassLoader 为默认的 ClassLoader（一般 Thread.currentThread()#getContextClassLoader() ）。
	 *
	 * @see java.lang.Thread#getContextClassLoader()
	 */
	public DefaultResourceLoader() {
		this.classLoader = ClassUtils.getDefaultClassLoader();
	}

	/**
	 * Create a new DefaultResourceLoader.
	 *
	 * 在使用带参数的构造函数时，可以通过 ClassUtils#getDefaultClassLoader()获取。
	 *
	 * @param classLoader the ClassLoader to load class path resources with, or {@code null}
	 * for using the thread context class loader at the time of actual resource access
	 *
	 */
	public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}


	/**
	 * Specify the ClassLoader to load class path resources with, or {@code null}
	 * for using the thread context class loader at the time of actual resource access.
	 * <p>The default is that ClassLoader access will happen using the thread context
	 * class loader at the time of this ResourceLoader's initialization.
	 *
	 * 也可以手动指定 ClassLoader,然后在进行后续设置
	 *
	 */
	public void setClassLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	/**
	 * Return the ClassLoader to load class path resources with.
	 * <p>Will get passed to ClassPathResource's constructor for all
	 * ClassPathResource objects created by this resource loader.
	 * @see ClassPathResource
	 */
	@Override
	@Nullable
	public ClassLoader getClassLoader() {
		return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
	}

	/**
	 * Register the given resolver with this resource loader, allowing for
	 * additional protocols to be handled.
	 * <p>Any such resolver will be invoked ahead of this loader's standard
	 * resolution rules. It may therefore also override any default rules.
	 *
	 * 调用 DefaultResourceLoader#addProtocolResolver(ProtocolResolver) 方法即可
	 *
	 * @since 4.3
	 * @see #getProtocolResolvers()
	 */
	public void addProtocolResolver(ProtocolResolver resolver) {
		Assert.notNull(resolver, "ProtocolResolver must not be null");
		this.protocolResolvers.add(resolver);
	}

	/**
	 * Return the collection of currently registered protocol resolvers,
	 * allowing for introspection as well as modification.
	 * @since 4.3
	 */
	public Collection<ProtocolResolver> getProtocolResolvers() {
		return this.protocolResolvers;
	}

	/**
	 * Obtain a cache for the given value type, keyed by {@link Resource}.
	 * @param valueType the value type, e.g. an ASM {@code MetadataReader}
	 * @return the cache {@link Map}, shared at the {@code ResourceLoader} level
	 * @since 5.0
	 */
	@SuppressWarnings("unchecked")
	public <T> Map<Resource, T> getResourceCache(Class<T> valueType) {
		return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>());
	}

	/**
	 * Clear all resource caches in this resource loader.
	 * @since 5.0
	 * @see #getResourceCache
	 */
	public void clearResourceCaches() {
		this.resourceCaches.clear();
	}


	// DefaultClassLoader 的核心方法
	@Override
	public Resource getResource(String location) {
		// 判断资源位置不能为 null
		Assert.notNull(location, "Location must not be null");

		// 首先，通过 ProtocolResolver 来加载资源
		for (ProtocolResolver protocolResolver : this.protocolResolvers) {
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
				return resource;
			}
		}

		// 其次，以 / 开头，返回 ClassPathContextResource 类型的资源
		if (location.startsWith("/")) {
			return getResourceByPath(location);
		}
		// 再次，以 classpath: 开头，返回 ClassPathResource 类型的资源.
		else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
			return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
		}
		// 然后，根据是否为文件 URL ，是则返回 FileUrlResource 类型的资源，否则返回 UrlResource 类型的资源
		else {
			try {
				// Try to parse the location as a URL...
				URL url = new URL(location);
				return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
			}
			catch (MalformedURLException ex) {
				// No URL -> resolve as resource path.
				// 也不是 URL 资源, 就采用 ClassPathContextResource 读取资源
				return getResourceByPath(location);
			}
		}
	}

	/**
	 * Return a Resource handle for the resource at the given path.
	 * <p>The default implementation supports class path locations. This should
	 * be appropriate for standalone implementations but can be overridden,
	 * e.g. for implementations targeted at a Servlet container.
	 *
	 * 子类可以通过重写getResourceByPath(String path)来返回和自身相关的资源类型
	 *
	 * @param path the path to the resource
	 * @return the corresponding Resource handle
	 * @see ClassPathResource
	 * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath
	 * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath
	 */
	protected Resource getResourceByPath(String path) {
		return new ClassPathContextResource(path, getClassLoader());
	}


	/**
	 * ClassPathResource that explicitly expresses a context-relative path
	 * through implementing the ContextResource interface.
	 *
	 * 有此类的原因: 实现 ContextResource 接口，并实现对应的 #getPathWithinContext() 接口方法。
	 *
	 */
	protected static class ClassPathContextResource extends ClassPathResource implements ContextResource {

		public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) {
			super(path, classLoader);
		}

		@Override
		public String getPathWithinContext() {
			return getPath();
		}

		@Override
		public Resource createRelative(String relativePath) {
			String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
			return new ClassPathContextResource(pathToUse, getClassLoader());
		}
	}


	// for test
	public static void main(String[] args) {
		DefaultResourceLoader resourceLoader = new DefaultResourceLoader();

		// UrlResource
		Resource urlResource = resourceLoader.getResource("file:/Users/pizhihui/Downloads/node_monitor.sh");
		System.out.println(urlResource instanceof UrlResource);

		// UrlResource
		Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");
		System.out.println(urlResource2 instanceof UrlResource);

		// ClasspathResource
		Resource classpathResource = resourceLoader.getResource("classpath:org/springframework/core/AliasRegistry.class");
		System.out.println(classpathResource instanceof ClassPathResource);

		// FileSystemResource
		Resource fileResource = resourceLoader.getResource("/Users/pizhihui/Downloads/node_monitor.sh");
		System.out.println(fileResource instanceof ClassPathResource);

		// 其实 DefaultResourceLoader 对#getResourceByPath(String) 方法处理其实不是很恰当，
		// 这个时候我们可以使用 org.springframework.core.io.FileSystemResourceLoader 。
		// 它继承 DefaultResourceLoader ，且覆写了 #getResourceByPath(String) 方法，使之从文件系统加载资源并以 FileSystemResource 类型返回，这样我们就可以得到想要的资源类型
		String path = "/Users/pizhihui/Downloads/node_monitor.sh";
		Resource resourceByPath1 = resourceLoader.getResourceByPath(path);
		FileSystemResourceLoader fileSystemResourceLoader = new FileSystemResourceLoader();
		System.out.println(resourceByPath1 instanceof ClassPathResource);
		Resource resourceByPath2 = fileSystemResourceLoader.getResourceByPath(path);

		System.out.println(resourceByPath2 instanceof FileSystemResource);

		System.out.println("end....");
	}


}
